/*
 * Copyright 2012 The Netty Project
 *
 * The Netty Project licenses this file to you under the Apache License,
 * version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at:
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package io.netty.handler.codec;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.ByteProcessor;

import java.util.List;

/**
 * A decoder that splits the received {@link ByteBuf}s on line endings.
 * <p>
 * Both {@code "\n"} and {@code "\r\n"} are handled. For a more general
 * delimiter-based decoder, see {@link DelimiterBasedFrameDecoder}.
 */
public class LineBasedFrameDecoder extends ByteToMessageDecoder
{

    /** Maximum length of a frame we're willing to decode. */
    private final int maxLength;

    /** Whether or not to throw an exception as soon as we exceed maxLength. */
    private final boolean failFast;

    private final boolean stripDelimiter;

    /** True if we're discarding input because we're already over maxLength. */
    private boolean discarding;

    private int discardedBytes;

    /**
     * Creates a new decoder.
     * @param maxLength the maximum length of the decoded frame. A
     *        {@link TooLongFrameException} is thrown if the length of the frame
     *        exceeds this value.
     */
    public LineBasedFrameDecoder(final int maxLength)
    {
        this(maxLength, true, false);
    }

    /**
     * Creates a new decoder.
     * @param maxLength the maximum length of the decoded frame. A
     *        {@link TooLongFrameException} is thrown if the length of the frame
     *        exceeds this value.
     * @param stripDelimiter whether the decoded frame should strip out the
     *        delimiter or not
     * @param failFast If <tt>true</tt>, a {@link TooLongFrameException} is
     *        thrown as soon as the decoder notices the length of the frame will
     *        exceed <tt>maxFrameLength</tt> regardless of whether the entire
     *        frame has been read. If <tt>false</tt>, a
     *        {@link TooLongFrameException} is thrown after the entire frame
     *        that exceeds <tt>maxFrameLength</tt> has been read.
     */
    public LineBasedFrameDecoder(final int maxLength,
            final boolean stripDelimiter, final boolean failFast)
    {
        this.maxLength = maxLength;
        this.failFast = failFast;
        this.stripDelimiter = stripDelimiter;
    }

    @Override
    protected final void decode(ChannelHandlerContext ctx, ByteBuf in,
            List<Object> out) throws Exception
    {
        Object decoded = decode(ctx, in);
        if (decoded != null)
        {
            out.add(decoded);
        }
    }

    /**
     * Create a frame out of the {@link ByteBuf} and return it.
     *
     * @param ctx the {@link ChannelHandlerContext} which this
     *        {@link ByteToMessageDecoder} belongs to
     * @param buffer the {@link ByteBuf} from which to read data
     * @return frame the {@link ByteBuf} which represent the frame or
     *         {@code null} if no frame could be created.
     */
    protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer)
            throws Exception
    {
        final int eol = findEndOfLine(buffer);
        if (!discarding)
        {
            if (eol >= 0)
            {
                final ByteBuf frame;
                final int length = eol - buffer.readerIndex();
                final int delimLength = buffer.getByte(eol) == '\r' ? 2 : 1;

                if (length > maxLength)
                {
                    buffer.readerIndex(eol + delimLength);
                    fail(ctx, length);
                    return null;
                }

                if (stripDelimiter)
                {
                    frame = buffer.readRetainedSlice(length);
                    buffer.skipBytes(delimLength);
                }
                else
                {
                    frame = buffer.readRetainedSlice(length + delimLength);
                }

                return frame;
            }
            else
            {
                final int length = buffer.readableBytes();
                if (length > maxLength)
                {
                    discardedBytes = length;
                    buffer.readerIndex(buffer.writerIndex());
                    discarding = true;
                    if (failFast)
                    {
                        fail(ctx, "over " + discardedBytes);
                    }
                }
                return null;
            }
        }
        else
        {
            if (eol >= 0)
            {
                final int length = discardedBytes + eol - buffer.readerIndex();
                final int delimLength = buffer.getByte(eol) == '\r' ? 2 : 1;
                buffer.readerIndex(eol + delimLength);
                discardedBytes = 0;
                discarding = false;
                if (!failFast)
                {
                    fail(ctx, length);
                }
            }
            else
            {
                discardedBytes += buffer.readableBytes();
                buffer.readerIndex(buffer.writerIndex());
            }
            return null;
        }
    }

    private void fail(final ChannelHandlerContext ctx, int length)
    {
        fail(ctx, String.valueOf(length));
    }

    private void fail(final ChannelHandlerContext ctx, String length)
    {
        ctx.fireExceptionCaught(
                new TooLongFrameException("frame length (" + length
                        + ") exceeds the allowed maximum (" + maxLength + ')'));
    }

    /**
     * Returns the index in the buffer of the end of line found. Returns -1 if
     * no end of line was found in the buffer.
     */
    private static int findEndOfLine(final ByteBuf buffer)
    {
        int i = buffer.forEachByte(ByteProcessor.FIND_LF);
        if (i > 0 && buffer.getByte(i - 1) == '\r')
        {
            i--;
        }
        return i;
    }
}
